#!/usr/bin/php
<?

/*
	This script watches a folder for op atom mxf files and converts them to prores hq mov files
	It depends on mxflib, ffmbc and mxfdump (included)
*/

// configs
$config['watchfolder']="/Users/fbogner/Desktop/MXFtest/data";
$config['outputfolder']="/Users/fbogner/Desktop/MXFtest";

$config['success_action']="move";	// "move" or "delete" files on successful conversion
$config['converted_folder']="/Users/fbogner/Desktop/MXFtest/converted";
$config['overwrite_existing']=true;	// overwrite destination if already existing

$config['pid_file']="/tmp/mxfmuxer.pid";

$config['wait_timeout']="2";	// timeout in seconds

// helper applications
$config['mxfdump']=dirname(__FILE__)."/mxfdump";
$config['grep']="/usr/bin/grep";
$config['awk']="/usr/bin/awk";
$config['system_profiler']="/usr/sbin/system_profiler";
$config['ffmbc']="/usr/local/bin/ffmbc";

$config['folder_hash']=false;


main();

// main function
function main() {
	debug("main function");
	
	global $config;

	if (!AmISingleton()) {
		info("There is already a process running");
		exit(1);
	}
	
	// create pid file
	debug("creating pid file");
	file_put_contents($config['pid_file'], getmypid());
	
	
	while(true) {
		debug("starting a new run");
		
		// get a hash of the folder contant
		$hash=getHashForMXFFileInFolder($config['watchfolder']);

		// if the hash has not changed size the last check no files have been changed
		// that means no new data is added so start the conversion
		// if no files are found at all don't do anything!
		debug("checking folder hash");
		debug("Current hash: $hash");
		if ($config['folder_hash']==$hash && $hash!=false) {
			info("starting conversion of new files");
			runConversion($config['watchfolder']);
			info("conversion finished");
		}
		else {
			info("files have been added or changed or now files have been found at all");
		}
		
		$config['folder_hash']=$hash;
		sleep($config['wait_timeout']);
	}
	
	// remove pid file
	debug("removing pid file");
	unlink($config['pid_file']);

}

function info($msg) {
	echo "$msg\n";
}

function debug($msg) {
	echo "$msg\n";
}


// returns false if there is already a process running
function AmISingleton() {
	debug("Checking if there is already an instance running");

	global $config;
	
	// check if pid file is available
	$pid=false;
	
	if (file_exists($config['pid_file'])) {
		$pid=file($config['pid_file']);
	}
	
	if ($pid===false) {
		return true;
	}
	else {
		$pid=$pid[0];
	}
	
	// check if pid is still running
	$isPIDValid=posix_getsid($pid);
	
	if ($isPIDValid===false) {
		return true;
	}
	else {
		return false;
	}

}

// returns a hash representing the folder content (false if no files found)
function getHashForMXFFileInFolder($path) {
	debug("creating hash for mxf files in folder");
	
	$files=getRecursiveMXFOPAtomFiles($path);

	debug("no files found");
	if (count($files)==0) {
		return false;
	}
	
	$hash="";
	debug("now creating hash");
	foreach ($files as $file) {
		$hash=md5("$hash$file".filesize($file));
	}
	
	return $hash;
}

// check's the given watch folder and convert's all MXF files to the output dir as MOV ProRes HQ
function runConversion($path) {
	debug("start data gathering for conversion");
	
	global $config;
	
	debug("get all mxf op atom files for path");
	$groups=getRecursiveMXFOPAtomFilesByUMID($path);

	foreach($groups as $group) {
		debug("try to convert group");
		$file=convertGroup($group);
	
		// check if it was successfull
		if ($file!==false) {
			debug("conversion successful");
		
			// process the mxf files (rename or delete)
			foreach($group as $oldfile) {
				// delete or move the old files
				if ($config['success_action']=="delete") {
					unlink($oldfile);
				}
				else {
					rename($oldfile,$config['converted_folder']."/".basename($oldfile));
				}
			}
		}
		else {
			debug("something went wrong during conversion");
		}
	
	}

}

// converts a given group to a single mov
// either returns false on failure or the new mov file
function convertGroup($group) {
	debug("Will convert group now!");
	
	global $config;
	$ffmbc=$config['ffmbc'];
	$cpu_count=getCPUCount();
	
	// check if valid
	if (!checkIfUMIDGroupIsValid($group)) {
		return false;
	}
	
	// generate output filename
	$name=getMOVNameForUMIDGroup($group);
	$name=$config['outputfolder']."/".$name;
	
	if (!$config['overwrite_existing'] && file_exists($name)) {
		info("skipping because destination $name already exists");
		return false;
	}

	// create command
	$cmd="$ffmbc  -y -threads $cpu_count";
	
	// add video input file
	$cmd="$cmd -i \"".getVideoTrack($group)."\"";
	//$cmd="$cmd -i \"".getVideoTrack($group)."\"";
	
	//audio processing
	$trackcounter=1;
	foreach(getAllAudioTracksStorted($group) as $audiofile) {
		$cmd="$cmd -i \"".$audiofile."\"";
		$trackcounter++;
	}	
	
	// add options
	$cmd="$cmd -acodec copy -vcodec prores -profile std -vf scale=0:0:interl=1,fieldorder=tff,scale=0:0:interl=1 \"$name\" -acodec copy";	//&>/dev/null
	
	for($i=0;$i<$trackcounter;$i++) {
		$cmd="$cmd -map ".$i.":0";
		
		if ($i>1) {
			$cmd="$cmd -acodec copy -newaudio";
		}
	}
	
	debug("will run command $cmd");
	
	// run converter
	$result=1;
	exec($cmd,$info = array(),$result);
	
	if ($result!=0) {
		debug("conversion failed with error code $result");
		return false;
	}
	
	debug("conversion successful");
	return $name;
}

// returns the number of physical cores as int
function getCPUCount() {
	debug("reading cpu count");
	
	global $config;
	$awk=$config['awk'];
	$system_profiler=$config['system_profiler'];

	$cpu_count=trim(shell_exec("$system_profiler -detailLevel full SPHardwareDataType | $awk '/Total Number Of Cores/ {print $5};'"));
	return $cpu_count;
}

function getVideoTrack($files) {
	debug("searching video tracks");
	
	foreach($files as $file) {
		
		if (!isMXFFileAudio($file)) {
			return $file;
		}
	
	}

	return $false;
}

function getAllAudioTracksStorted($files) {
	debug("searching and sorting audio tracks");

	$audio=array();
	
	foreach($files as $file) {
		
		if (isMXFFileAudio($file)) {
			array_push($audio,$file);
		}
	
	}

	sort($audio);
	
	return $audio;

}

// generates the MOV name (no path!) of a given UMID group based on the video name or the metadata
function getMOVNameForUMIDGroup($files) {
	// try to generate from metadata
	$name=getMOVNameForUMIDGroupFromMetadata($files);
	
	if ($name!==false) {
		return $name;
	}
	
	// try to generate from filename
	$name=getMOVNameForUMIDGroupFromFilename($files);
	
	if ($name!==false) {
		return $name;
	}
	
	return false;
	
};

// generates the MOV name (no path!) of a given UMID group based on the metadata
function getMOVNameForUMIDGroupFromMetadata($files) {
	debug("creating mov name for group from metadata");

	global $config;
	$mxfdump=$config['mxfdump'];
	$grep=$config['grep'];

	$file=getVideoTrack($files);
	
	if ($file!==false) {
			
		$cmd="$mxfdump -x1 \"$file\"|$grep \"Name = 00302\"";
		$name=trim(shell_exec($cmd));
		
		// remove prefix
		$name=str_replace("Name = 00302 ","",$name);
			
		if ($name!="") {
			return $name.".mov";
		}
		else {
			return false;
		}
	}
	
	debug("no video file for filename generation found!");
	return false;
}

// generates the MOV name (no path!) of a given UMID group based on the video name
function getMOVNameForUMIDGroupFromFilename($files) {
	debug("creating mov name for group from filename");

	$file=getVideoTrack($files);

	if ($file!==false) {

		$fileinfo = pathinfo($file);
		$filename=$fileinfo['filename'].".mov";

		debug("MOV name $filename generated");
		return $filename;
	}
	
	debug("no video file for filename generation found!");
	return false;
}

// checks if an array of files contains a single video file and one or two audio files (returns a bool)
function checkIfUMIDGroupIsValid($files) {
	debug("checking if group is valid");
	
	$video=0;
	$audio=0;

	// find out what is audio and what is video
	foreach($files as $file) {
		if (isMXFFileAudio($file)) {
			$audio++;
		}
		else {
			$video++;
		}
	}

	// check against requirements
	if ($video==1 && $audio>=0) {
		debug("success $video video files and $audio audio files found");
		return true;
	}
	else {
		debug("failure $video video files and $audio audio files found");
		return false;
	}
}

function isMXFFileAudio($file) {
	debug("checking if $file is from type audio");
	
	global $config;
	$mxfdump=$config['mxfdump'];
	$grep=$config['grep'];

	$cmd="$mxfdump -g \"$file\"|$grep EssenceContainer";
	$type=shell_exec($cmd);
		
	if (strpos($type,"Audio")!==false) {
		//it's audio
		debug("it is audio");
		return true;
	}
	else {
		//it's video
		debug("it is video");
		return false;
	}
}

// returns an array of umids containing an array of corresponding files
function getRecursiveMXFOPAtomFilesByUMID($path) {
	debug("grouping alls mxf op atom files for path $path");

	$opatomfiles=getRecursiveMXFOPAtomFiles($path);
	$groups=array();

	foreach($opatomfiles as $file) {
		$umid=getUMID($file);
		if ($umid==false) {
			continue;
		}
		
		// check if umid group exists
		if (!isset($groups[$umid])) {
			$groups[$umid]=array();
		}
		
		// add file to umid group
		array_push($groups[$umid],$file);
	}

	return $groups;
}

// returns the umid for a given mxf file
function getUMID($file) {
	debug("getting umid for file $file");

	global $config;
	$mxfdump=$config['mxfdump'];
	$grep=$config['grep'];

	// get the umid using mxfdumo
	$cmd="$mxfdump -g \"$file\"|$grep ffdd41e1-ae2c-49c6-ae58-78e041454179";
	$umid_long=shell_exec($cmd);
	
	// strip away the unnecessary stuff
	$pos = strpos($umid_long,"=")+1;
	$umid=substr($umid_long,$pos,strlen($umid_long)-$pos);
	$umid=trim(str_replace(" ","",$umid));
	
	if ($umid=="") {
		return false;
	}
	else {
		return $umid;
	}
}

// returns an array of mxf op atom files in the folder and it's subfolders
function getRecursiveMXFOPAtomFiles($path) {
	debug("getting alls mxf op atom files for path $path");

	global $config;
	$mxfdump=$config['mxfdump'];
	$grep=$config['grep'];
	
	$mxffiles=getRecursiveMXFFiles($path);
	$opatomfiles=array();
	
	foreach($mxffiles as $file) {
		$cmd="$mxfdump -g \"$file\"|$grep OperationalPattern";
		$pattern=trim(strtok(shell_exec($cmd), "\n"));
		
		if (strpos($pattern,"OP Atom")!==false) {
			array_push($opatomfiles,$file);
		}
	}

	return $opatomfiles;
}

// returns an array of mxf files in the folder and it's subfolders
function getRecursiveMXFFiles($path) {
	debug("getting alls mxf files for path $path");

	$allfiles=getRecursiveAllFiles($path);
	$mxffiles=array();
	
	foreach($allfiles as $file) {
		
		$ext=pathinfo($file, PATHINFO_EXTENSION);
		if (strtolower($ext)=="mxf") {
			array_push($mxffiles,$file);
		}
	}

	return $mxffiles;

}

// returns an array of all files in the folder and it's subfolders
function getRecursiveAllFiles($path) {
	debug("getting alls files for path $path");

	$allfiles=array();

	$objects = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path));
	foreach($objects as $name => $object){
    	array_push($allfiles,$name);
	}

	return $allfiles;

}

?>